﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Actors;
using System.Threading.Tasks;
using System.Dynamic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;

namespace TestActorMethods
{
    [TestClass]
    public class DynamicUnitTests
    {
        [TestMethod]
        public void DynamicState_Test()
        {
            DynamicState_Test_impl(ActorStateEnum.Standalone);
            DynamicState_Test_impl(ActorStateEnum.InMemory);
        }

        public void DynamicState_Test_impl(ActorStateEnum stateType)
        {
            IActorState state = ActorUnitTests.CreateActorState(stateType);

            dynamic dstate = new DynamicActorStateWrapper(state);

            // Property-style get/set
            state.Set("foo", 1);
            int iresult = dstate.foo;
            Assert.IsTrue(iresult == 1, "Failed property-style get");
            dstate.foo = 13;
            iresult = (int)state.Get("foo");
            Assert.IsTrue(iresult == 13, "Failed property-style set");
            dstate.foo += 1;
            iresult = (int)state.Get("foo");
            Assert.IsTrue(iresult == 14, "Failed property-style increment");

            // Index-style get/set
            state.Set("bar", 10);
            iresult = dstate["bar"];
            Assert.IsTrue(iresult == 10, "Failed index-style get");
            dstate["bar"] = 42;
            iresult = (int) state.Get("bar");
            Assert.IsTrue(iresult == 42, "Failed index-style set");
            dstate["bar"] += 2;
            iresult = (int) state.Get("bar");
            Assert.IsTrue(iresult == 44, "Failed index-style increment");

            // Contains method
            Assert.IsTrue(dstate.Contains("bar"), "Failed Contains() test that should have been true");
            Assert.IsFalse(dstate.Contains("spam"), "Failed Contains() test that should have been false");

            // ValueOrDefault method
            dstate.bar = 101;
            iresult = dstate.ValueOrDefault("bar", -1);
            Assert.IsTrue(iresult == 101, "Failed ValueOrDefault() test for value that should have existed");
            iresult = dstate.ValueOrDefault("spam", 13);
            Assert.IsTrue(iresult == 13, "Failed ValueOrDefault() test for value that should not have existed");

            // Remove method
            dstate.Remove("bar");
            Assert.IsFalse(dstate.Contains("bar"), "Failed Contains() test after explicit removal of key");
            iresult = dstate.ValueOrDefault("bar", 57);
            Assert.IsTrue(iresult == 57, "Failed ValueOrDefault() test after explicit removal of key");

            // Add a method dynamically
            dstate.DMethod = (Func<dynamic, dynamic[], object>)delegate(dynamic ds, dynamic[] args)
            {
                string keyName = args[0];
                ds[keyName] = args[1];
                return 42;
            };

            // Call the dynamically-created method statically
            iresult = (int)state.CallMethod("DMethod", new object[] { "Key1", "Key1Value" });
            Assert.IsTrue(iresult == 42, "Wrong return value from statically called dynamic method");
            string s = (string)state.Get("Key1");
            Assert.IsTrue(s.Equals("Key1Value"), "Wrong keyvalue set from statically called dynamic method");

            // And call the dynamically-created method dynamically
            iresult = dstate.DMethod("Key2", "Key2Value");
            Assert.IsTrue(iresult == 42, "Wrong return value from dynamically called dynamic method");
            s = dstate.Key2;
            Assert.IsTrue(s.Equals("Key2Value"), "Wrong keyvalue set from dynamically called dynamic method");

            // Add a method statically
            state.Set("SMethod", (Func<IActorState, object[], object>)delegate(IActorState ss, object[] args)
            {
                var keyName = (string)args[0];
                ss.Set(keyName, args[1]);
                return 13;
            });

            // Call the statically-created method statically
            iresult = (int)state.CallMethod("SMethod", new object[] { "Key3", "Key3Value" });
            Assert.IsTrue(iresult == 13, "Wrong return value from statically called static method");
            s = (string)state.Get("Key3");
            Assert.IsTrue(s.Equals("Key3Value"), "Wrong keyvalue set from statically called static method");

            // And call the statically-created method dynamically
            iresult = dstate.SMethod("Key4", "Key4Value");
            Assert.IsTrue(iresult == 13, "Wrong return value from dynamically called static method");
            s = dstate.Key4;
            Assert.IsTrue(s.Equals("Key4Value"), "Wrong keyvalue set from dynamically called static method");

            // The rest of these tests only work well on StandaloneActorState
            if (stateType == ActorStateEnum.InMemory) return;

            // GetActorProxy tests
            IActorProxy proxy = dstate.GetActorProxy("SomeActor");
            var observable = proxy["SomeEventType"];
            var disposable = observable.Subscribe("SomeHandler");
            disposable.Dispose();

            // Make sure that the linkage is there to create and delete actors from a dynamic actor method
            state.Set("actorCreator", (Func<dynamic, dynamic[], object>)delegate(dynamic astate, dynamic[] args)
            {
                string newActorName = args[0];
                astate.CreateActor(newActorName, "nothing");
                var aproxy = astate.GetActorProxy(newActorName);
                return null;
            });
            state.Set("actorDestroyer", (Func<dynamic, dynamic[], object>)delegate(dynamic astate, dynamic[] args)
            {
                string newActorName = args[0];
                astate.DeleteActor(newActorName);
                return null;
            });

            dstate.actorCreator("createdActor");
            StandaloneActorState lookedUpState = null;
            Assert.IsTrue(StandaloneActorState.TryLookupActorState("createdActor", null, out lookedUpState));
            dstate.actorDestroyer("createdActor");
            Assert.IsFalse(StandaloneActorState.TryLookupActorState("createdActor", null, out lookedUpState));
        }

        [TestMethod]
        public void DynamicProxy_Test()
        {
            StandaloneActorState state1 = new StandaloneActorState("Actor1");
            StandaloneActorState state2 = new StandaloneActorState("Actor2");

            dynamic dstate1 = new DynamicActorStateWrapper(state1);

            state1.Set("ResultHandler", (Func<IActorState, object[], object>)delegate(IActorState state, object[] args)
            {
                var promise = (IActorPromise)args[0];
                Thread.Sleep(500);  // Explicitly help to trigger a race condition, if the test doesn't block on ContinueWith's returned task.
                state.Set("result", promise.GetTypedResult<int>());
                return null;
            });
            state2.Set("Method1", (Func<IActorState, object[], object>)delegate(IActorState state, object[] args)
            {
                return 42;
            });

            // Grab our dynamic IActorProxy
            var dproxy = dstate1.GetActorProxy("Actor2");

            // Test direct call of non-existent method
            Exception e = null;
            try
            {
                IActorPromise bogusPromise = (IActorPromise) dproxy.NonExistentMethod("foo", "bar");
                Console.WriteLine("bogusPromise result = {0}", bogusPromise.Result); // Should throw an exception
            }
            catch (Exception exc)
            {
                e = exc;
            }
            Assert.IsNotNull(e, "Expected an exception on direct non-existent method call");

            // Test direct method call on dynamic proxy (w/ synchronous Result reading)
            int method1Result = dproxy.Method1().GetTypedResult<int>();
            Assert.IsTrue(method1Result == 42, "Expected result to be \"42\" after direct Method1 call w/ Result");

            // Test direct method call on dynamic proxy (w/ continuation-based result reading)
            Assert.IsTrue(dstate1.ValueOrDefault("result", 0) == 0, "Did not expect \"result\" to be set yet");
            var dpromise = dproxy.Method1();
            IActorPromise continuation = dpromise.ContinueWith("ResultHandler");
            Object obj = dpromise.Result;  // Wait for the original task to complete.  This is optional.
            // We are testing the side effect of the continuation here, so we must wait for the continuation task.
            continuation.Wait();
            int storedResult = dstate1.ValueOrDefault("result", 0);
            Assert.IsTrue(storedResult == 42, "Expected \"result\" to be set to 42 after direct Method1 call w/ continuation and waited on the promise.  Stored result was: " + storedResult);

            // Test indirect method call on dynamic proxy (w/ synchronous result reading)
            method1Result = dproxy.Request("Method1").GetTypedResult<int>();
            Assert.IsTrue(method1Result == 42, "Expected result to be 42 after indirect async Method1 call w/ Result");

            // Test conversion to static IActorProxy
            IActorProxy explicitProxy = dproxy;
            method1Result = explicitProxy.Request("Method1", new object[0]).GetTypedResult<int>();
            Assert.IsTrue(method1Result == 42, "Expected result to be 42 Method1 call/result on static IActorProxy");

            // Test that Subscribe is supported in a chained fashion
            dproxy["SomeStream"].Subscribe("SomeMethod");
            
        }
    }
}
